Hey there.
In this lecture, we're going to be learning about a crucial service that allows us to store data that
we need to persist between client sessions.
This data can be loaded and saved each time a player joins and leaves our game, so they never lose
their progress.
Data stores can be a scary topic for some, but the API for data stores is actually quite simple to
use and easy to implement in your games.
Before we get started, we must first note that testing in studio does not have access to data stores,
so we first have to grant permission to store data in studio.
To do so, we can go up to file, enter into our game settings and open up the security tab.
And here we can enable studio access to API services such as data stores.
So go ahead and enable that and press save.
Now we can go ahead and get started learning about data stores.
To understand data stores.
What I'm going to do is set up a leaderboard.
In my game.
You probably don't know how to set up a leaderboard, so we're going to get to go through that as well.
We can create a new script inside of our server script service, and I'm going to call this script my
leaderboard handler.
Inside of the script, we're going to need access to the player service.
That way we can listen for when players leave and join our game.
Inside of the player service.
We're going to index for the player added event and connect a Lambda function to it and get that player
that has joined our game.
When a player joins our game, we're going to want to set up a leaderboard for them.
So let's go ahead and create a function for doing that.
I'm going to create a local function and I'm going to call this setup leaderboard for.
And then we can pass a player instance to this function.
What this function is going to do is it's going to create a new folder.
So we can create a variable.
I'm going to call it folder using the instance dot new function to create a new folder and the name
of this folder.
In order for it to store leaderboard data, it has to be equal to the name of leader stats.
No capital letters, no spaces.
It must be named exactly like this.
Afterwards, we can set the parent.
Of this folder equal to our player.
Now to actually store information in this, what we can do is we can create some, uh, value instances
like number values or string values, and those will display inside of our leaderboard.
So for this example I want to create, let's say a new number value that's going to represent the player's
money.
So we can create a variable I'll call it money.
And we'll create a new number value.
And we're going to set the name of this number, value equal to whatever we would like to display on
our leaderboard.
In this case we'll just call it money.
And then we can go ahead and set the parent of money equal to our players Leaderstats folder.
So we can actually just reference our folder variable here.
And that's all we need to do to set up a money number value for our players leaderboard.
Then we can call this function within our lambda function and pass the player.
And now every time a player joins our game, we're going to set up, uh, an individual, uh, money
number value for each player.
Now, since this event may not fire while we're testing in studio because our player joins the game
first, then what we could do is we could loop through every single player inside of players, get players.
And then we can set up the leaderboard for each one of those players.
So now we have a basic leaderboard system set up.
And if we go and playtest our game.
As you can see now we have a money category in the leaderboard and currently my value is set to zero.
However, if we go to the server and if the server changes the value, as you can see, here's our Leaderstats
folder.
And then here's our money number value.
If the server updates this value to, let's say we now have $100 and we go back to the client.
As you can see it updates within our leaderboard as well.
So when you have a leaderboard in your game, there's many cases where you would actually like to save
this data and reload the data next time this player joins your game.
And this is what we use data stores for.
So in order to implement a data store into our server script, we're going to need to get the data store
service.
So we'll create a new variable.
We'll call it data store service.
And it's going to be equal to Game Get service data Store service.
Now inside of this data store service we have functions to create or get a data store.
So what I'm going to do is I'm going to create a variable.
And this variable is going to represent my money data store.
So I can call it money data store.
And inside of the data store service, there is a function we could call, which is called Get Data
Store and it says creates a data store instance with the provided name and scope.
Now, if this data store with this name does not exist, then it will create it.
However, if this data store already exists in your game with this name, then instead it will return
that data store instead of creating a new one.
So we can go ahead and name this data store for example player money.
So now we have a money data store that we can load and save data inside of any time a player leaves
or joins our game.
So what I'm going to do now is I'm going to create two functions.
I'm going to call this function get money data for.
And we can pass a player instance to this function.
And I'm going to create another function.
And I'm going to call this one save money data for.
And we'll also pass a player instance to this function.
Now, in order to save data to our data store, we first need to set a key that's going to be associated
with a value.
Because data stores are basically like dictionaries, data stores have a key, and that key is associated
with a value.
Now, since we want the data store to store unique data for every single player that plays our game,
then we need to figure out how to create a special key for each player.
So that way when they leave or come back into the game, we always get their same data.
Now, you might think of using the players name as the key for the data store, but this is actually
a bad practice, and that's because players are able to change their name, which means if a player
plays your game, saves data, but then later they change their name and come back to your game, they
won't have access to their data anymore.
So the best thing to use for saving player data is going to be the player's user ID, which is unique
for every single player in your game.
So when we want to save some money for our player, we can pass the player, and then we can also pass
the amount of money that we would like to save for this player.
Now to save data.
There's a function in our money data store called set a sync and it says sets the value of the data
store for the given key.
So you pass a key and you pass a value.
And that key will be given that value inside of your data store.
Now this is an easy and quick way to set data inside of your data store.
If you do not care about the previous value that was stored at this key.
However, in times when you want to prevent data loss in your data stores, it may be more smart to
use the update async function instead.
And what this does is it allows you to see the previous value stored at a particular key, and from
that point, you can make decisions on whether or not you want to save the data or not.
For example, maybe the player joined your game and you failed to load data for them and you want to
go update their data.
If you notice that their current data in game is off to what is saved in your data store, then you
can be like, ah, I don't want to update data for this player and avoid data loss.
So in most cases I'd recommend using update async, but you do want to be mindful of the limitations
of data stores outlined in the Roblox API documentation.
Inside of the Roblox documentation, there's a section talking about limits for data stores.
It states that there are also limits applied to the data store model.
If an experience exceeds these limits, the service automatically throttles the experiences.
Data store usage, causing requests to be placed in a queue.
When a limit is reached, further requests are placed into one of four queues set, ordered, set,
get and ordered Get requests in a queue are handled in the order they are received, and the called
function continues to yield as long as its request is queued.
If the data store key itself is throttled, the request is temporarily skipped, but still in the queue.
Each queue has a limit of 30 requests, and when this limit is exceeded, the request will fail with
an error code and the 301 to 306 range indicating that the request was dropped entirely.
So here Roblox shows us the different limits with the different functions inside of our data store.
So as you can see, for example, our set async would count against the set limit in our data store.
There's another function here called get async, which allows us to grab the value stored at a particular
key.
And this function counts against the get limit.
So here is the limit that we are given for each one of our servers.
It's equal to the number of players multiplied by ten.
And this is per minute.
So each player is allowed ten requests per minute plus an additional 60 in case you need them.
So every single minute that passes by in your game, each player in your game is allowed ten requests
to a data store for the get set, and the other values down here that we're not going to worry about
too much.
These ones are given less requests, but the ones that you're going to be using the most are these ones
up here.
And you get ten requests per each play in your game, plus an additional 60.
Another area we want to be mindful of with our data stores are throughput limits.
So as you can see here, every single minute for each of our servers, we have a 25 megabyte limit for
reading and a four megabyte writing to data stores.
So they get async function will count against your read limit, while for example, the set or remove
async functions will count against your write limit.
However, the update async function, since it takes into account both the previous value and overwriting
it with a new value.
That function will count against both your read and your write limit.
So you just want to be mindful about the amount of data that you're saving to your data stores, and
make sure you do not exceed the 25 megabyte per minute read or the four megabyte per minute write throughput
limits.
You won't likely ever reach these limits unless you're storing a lot of data for some reason, so I
wouldn't worry too much about this.
But again, you always want to be mindful with how frequently you are calling the different functions
for your data stores.
So to save data for this player, we can use set async and the API for this is very simple.
You would just pass the key.
In this case we will be using the player's user id and then the value that we would like to store at
this key would be the amount passed to this function.
And that's how you would use the set async function.
Now to use the update async function it's a little bit different.
You pass the key, but this time you need to pass a function.
And what this function does is that it gets passed the previous data that was stored at this particular
key, and we'll just call it old data here in the parameter section for our function.
From this point, you can check anything you would like with this old data.
You can make sure that everything looks good.
The player didn't error previously or whatever, and if everything is good to go, then you can update
the previous data with whatever value you would like.
In this case, I would like to update all data with this new amount, and then you would return this
new data back to when this lambda function gets called internally by the update async function.
So this is how you would use update async to get and then set player data for a particular key.
Now something very important to note about these async functions is that they are a synchronous and
asynchronous functions are prone to errors.
And this is because since these interact with Roblox cloud, there could be instances where an error
occurs online somewhere that's completely out of your control.
So you want to prevent your scripts from halting execution and encountering an error.
And this is where we would use the pcall or protected call function.
Right.
So we can call Pcall.
And then we can pass the lambda function in here.
And what we want to do is we want to copy this here and then just paste that inside of our pcall.
So that way if this does error, our pcall is going to catch it and it's not going to halt the execution
in our script.
And of course, we can also store the success and then the error from this pcall.
And we can check whether or not if we were successful.
So if we were not successful in storing this player's data, then we can go ahead and warn in the console
that we failed to save player data for a particular player, and we could pass some extra information
like the player's user ID as well as the error that occurred.
And that's how you would save data for a player using the update async function.
Now, how about loading or getting data for a player?
Well, that's very easy as well.
What we would do is we would use inside of our money data store the git async function, and we would
simply pass the key, which would be our player's user ID, and that's it.
This will return back to us whatever value is stored at this key.
However, this function is also prone to errors, so that means you need to wrap it in a p call.
So we'll do this exact same thing here.
And we'll copy this.
But instead what we want to do is we want to return the value from this get async function call.
So we'll just put a return statement here and there we go.
Now we can store whether or not this call was successful.
And then the result from the p call.
If the p call errors then this result is going to store our error message.
However, if we're successful then this result variable is going to store whatever value was stored
inside of our data store.
So then we can go ahead and check if we were successful and if we were, we can go ahead and return
the result.
Otherwise, if we were not successful, we could say something like failed to grab money data for player.
And again we could pass the player's user ID and then whatever the error message was.
And since we don't want to return nothing from this function, we'll just go ahead and return the value
of zero.
So now using these two functions we can go ahead and update our code.
For example, here, when we set up a leaderboard for a player, we want to update the value inside
of this money number value to whatever is stored in the data store.
So when we create this new money instance, set the name equal to money.
Then we can set the value inside of here equal to what gets returned from our get money data for function.
And we'll pass our player here.
So now this function will either return whatever value is stored in the data store, or the value of
zero by default.
And actually, one more thing we need to check for is that this result that is returned from our Get
async function actually might be nil, because if a new player joins your game, obviously they're not
going to have any data stored in this data store.
So when they get async function is called it might return nil.
But the p call will say that it was successful.
So what we need to check for is that we also got a result from our p call.
So what we could do is if we were successful, then what we're going to do is we're going to check if
we have a result.
So if we have or if we don't have a result, if we don't have a result, then we need to set result
equal to a default value of like zero.
And then we can go ahead and return the result.
So this will catch when we don't have any data stored for new players that join our game.
Now the next thing we can go ahead and listen to is when a player is removed from our game.
So inside of the player service, we can access the player removing event and connect a function to
this and get the player that is leaving our game.
And then we can go ahead and call our save money data for event.
We can pass our player, and then we can go ahead and get the amount that we would like to save.
And again, that's going to be stored inside of the player's leaderstats folder.
So we can index the player for Leaderstats and then get the money number value.
And then we can go ahead and pass the value that is stored in that instance.
So now any time a player joins our game or leaves our game, we're going to be loading or saving data
for them.
So now if we go and play test our game.
Here we get the value of zero for our money.
If I go to the server and I decide to update our money to, let's say, $100.
Now we have $100 for my player, and then if my player decides to leave the game, then that's going
to be stored inside of our data store.
And that means if I play my game again and join a new session, we should get that money back inside
of our data store.
And just like that, as you can see, our money of $100 is back with us in our game.
And that's how you use data stores.
You just store data in the cloud, and then you take that data back whenever you need it.
Of course, there are limits that you need to be aware of, but most of the times you won't exceed those
limits unless you're doing something wrong in your code.
Now, there are a couple of other things we need to account for when saving player data.
For example, let's say all the servers in your game get shut down because you've just passed a new
update and you wanted to close all the servers and start all fresh ones for the new update.
Well, there might be a case where we won't be able to save data for all of the players in your game,
and that's a problem.
So to fix that, if we were to refer to game, there's a function in there called bind to close and
it says binds a function to be called before the game shuts down.
So before any of your servers shut down, this function that is passed in here is going to execute.
And this is where we could go ahead and save all of the data for the players in our server before the
game closes.
We may also want to save player data periodically, like every five minutes, to account for possible
crashes, because if a server crashes, this bind to close function will not execute.
So another thing that developers do is that they create a while true do loop, and they have a forever
loop running that runs, let's say every five minutes.
So we would wait 300 seconds.
So every five minutes it goes and saves all the data for the players in our game.
Now, since we want both of these, um, blocks of code to do the same thing, we can go ahead and create
a function specifically for saving all of the money data for all of the players in our game, so we
can create a new function.
I'm going to call this function.
Uh, we'll call it save money data for all players.
And all this function is going to do is it's going to loop through every single player in our game.
So we're going to do players get players.
And what we want to do is we want to save their money data.
So we would call the save money data for function and pass this player.
Now the problem is, is that the update async function and the get async function they yield.
So when the threat of execution hits this block of code, it's going to be sitting here waiting right
here and also right here.
And if the game is shutting down or we want to save all the data for all the players in our game, we
don't want to have to sit here and yield for every single player as we save their data.
So the best thing that we could do is we could use the task library and use the spawn function to spawn
this function in a new thread.
That way we aren't halting the thread of execution in this for loop.
So now we have a function that goes through all of the players in our game and saves all of the money
data for these players.
And we also need to make sure that we pass the amount as well.
So again we'll refer to our player, get their leader stats folder, get the money number value that
is in there, and then get the value stored in that instance.
And now we can go down and call this function here.
And then we can also go ahead and call this function here.
Now we have a complete data source system set up where it goes, and it saves data for all the players
in our game every five minutes.
It saves all the data in our game.
When the game closes down.
We also get all of the data for players when they join the game.
And then we also save data for players when they leave the game.
One more thing that we need to account for is let's say we grab money data for a player, but it errored
and we failed to grab their data.
Well, if a player, let's say, had $1 million in your data store, but then an error occurred and
we weren't able to grab their data and instead we just returned zero.
That means now we're storing the value of zero inside of that number value money instance.
And then when that player leaves the game, we're overwriting their data with this new value of zero,
which could result in data loss.
So to prevent that issue, what we can do is we can create a table.
And I'm going to call this table Errored players.
And any time we use the get money data for function and we fail to grab the data for a particular player,
then we can store that player inside of our Arab players table.
So that way, any time we call this function right here, we can check if that particular player errored.
And if they did, we're not going to save their data.
So right down here, if we have failed to grab money data for this player, then inside of our Arab
players table, we'll create a new key using the players name.
And we're going to set this equal to true to signify.
Yep, they aired that way.
Any time we call this function, we could go ahead and check if inside of the aired players table this
player exists.
And if they did, then we can go ahead and just exit out of this function.
And we can also give a warning like cannot.
Save data for Errored players.
One last thing we need to mention is that when you're play testing in studio and you set up, for example,
a multiple player server, the players that are going to be present in these test servers are not actual
real players.
They're user IDs are going to be of a negative value, and you do not want to store negative values
as keys inside of your data stores, because that's going to cause problems.
So any time when you enable, um, access to API services inside of your game, you need to be very
careful to not store any user IDs that are negative.
And we can easily prevent that from happening by going to our save money data for function and checking
if this player's user ID is negative.
So.
If this player dot user id is less than, let's say one, then we can go ahead and warn and say something
like unable to save data for non real player accounts.
And then we'll just return out of this function.
We can also basically do the exact same thing in our Get Money data for function two.
If this player has a negative user ID, meaning it's a non real player account, then we can say unable
to grab data for non real player accounts and then we'll just return like zero.
So now if we go and play our game, everything should still be functioning fine.
As you can see, we still have my money right here.
All is well.
And then if I, um, actually a cool demonstration that we could do is we can purposefully have this
section of our code error.
So what I'm going to do is I'm just going to type some random stuff in there or actually, no, I'll
call the error function to force an error to occur right here.
Which will result in this boolean being false.
And that means we're going to be set inside of this table.
And I want to test to see whether or not my data gets overwritten or not.
So if I go and play test the game.
As you can see, that error occurred and we got failed to grab money data for player.
It passed our ID and then it gave us the error which was hahahahaha.
And as you can see now my money value is at zero, which kind of sucks.
And then if I go and leave the game, hopefully it's not going to save my data.
And instead a warning should spit out saying we cannot save data for Arab players.
So if I leave.
There we go.
Cannot save data for error players and we prevented that data loss from occurring.
So if I go and remove this error and then I go and play my game again.
As you can see, I still have my $100 stored in the data store.
So that is how we can use the data store service to load and save data for players in our game.
In the next lecture, we're going to be taking a look at how to grab the players with the most money
on our game using ordered data stores.
See you there!